我相信对于所有的iOSer来说,最恐怖的就是线上的crash了。对于有(mei)强迫(you)症(qian)的我来说,只要一发现有crash,只要条件允许,无论是在凌晨三点还是在晚上十二点,我都会立刻拿出电脑来查找原因看看是哪里导致的crash。所以为了让程序不要crash,我们有两种方法,一种是预防,一种是补救。
今天我们的重点是预防,至于补救我在之前的一篇文章中有说过,大体就是通过上传crash堆栈信息,或者通过xcode拿到log,然后通过符号表还原定位到crash的代码。具体的可以看iOSCrash信息上报和处理
1. crash的原因
我们最常见的crash的原因不外乎以下几种:
- unrecognized selector sent to instance
- KVO crash
- NSNotification crash
- NSTimer crash
- 数组越界,字典插入nil
- 字符串处理crash
- EXC_BAD_ACCESS
unrecognized selector sent to instance这个crash我相信所有的开发者都见过,这种问题一般在开发或者测试的时候可以发现并且很容易就修复了。
但是有一天,我们亲爱的服务端同事,本来是返回一个person的name属性,但是那个name是空的,在OC中,nil
代表的是空,但是服务端返回的可能是一个“<NULL>”
,OC解析出来会是一个NSNull
的对象。如果我们在代码里面还有对name属性做一些字符串的处理,例如[name subString:]
,毫无疑问,这个APP会自杀给你看。
2. unrecognized selector的背后
拿上面的[name subStringFromIndex:1]
来说,我们知道OC中的调用方法不是单纯的直接获取到方法函数的地址直接调用,而是一个消息发送的过程。先放一张图:
上面是消息转发的流程,也就是我今天重点要说的。
但是其实在这张图之前还有一个消息查找的流程,消息查找的流程如下:
- 如果是对象方法,首先会在对象的缓存方法里面查找,如果没找到则根据对象的isa指针,去类对象里面查找。
- 如果类对象里面没有查找到该方法,则会在该类对象的父类里面查找,查找遵循1,2的顺序
- 如果一直查找到根类都没有找到该方法的实现的话,就会开始消息的转发流程,也就是上图的流程
- 如果消息的转发依然没有找到对应的处理,则会crash。
3. 在哪里做拦截处理
如果我们要防止程序unrecognized selector crash,我们可以在消息的转发流程中做拦截处理,我们先来看看runtime给我们提供的三个消息处理的方法。
1. + (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
这个是消息转发的第一个方法,开发者可以在这个方法里面利用class_addMethod给该对象提供一个方法的实现,然后返回YES。如果返回了NO或者则会调用下面第二个方法。
2. - (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
当系统调用到该方法的时候,需要开发者返回一个id对象,如果返回了一个非nil的对象,那么系统会将该selector转发到该对象上去,也就是会重新开始消息的发送流程。如果返回了nil或者self,那么就会执行下面的第三步
`3. - (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE(“”);
- (NSMethodSignature )methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE(“”);`
**系统会首先调用methodSignatureForSelector
来获取一个方法的签名,如果返回了方法的签名则会创建一个NSInvocation对象并且调用forwardInvocation
将消息转发给目标对象。***
接下来就有一个疑问了,我们应该在哪一步里面拦截消息会比较好。
- resolveInstanceMethod方法需要我们为该类添加一个实现,这是不必要的,因为本来那个类就不需要有那个方法,没必要为了防止crash而给它添加一个多余的实现。
- methodSignatureForSelector 以及 forwardInvocation 这里的开销比较大,需要创建invocation对象并且这里有可能会被开发者调用进行消息多重转发机制,所以这里也不太适合。
- 剩下的就只有forwardingTargetForSelector方法了,这个方法需要提供一个处理消息的类,我们正好可以创建一个用于保护的
LMProtecter
类,到时由该类里面处理就可以了。
4. 拦截的流程
我们拦截处理的流程大概有以下几点:
- 利用method_swizzle,创建一个NSObject的分类在load方法里面替换原来的forwardingTargetForSelector。
- 提供一个白名单机制,只对特定的类进行处理,例如常见的NSNull或或者是SDK中的类,通过类前缀来判断该类是否属于SDK的类。如果是系统的类(一般以_开头,则不进行拦截)
- 创建一个protect类,并且重写protect类的resolveInstanceMethod方法,提供一个方法的实现,当消息转发给protect类的时候,执行默认的实现,该方法会有一个返回值返回NSNull,这样当执行某些有返回值的方法的时候,可以避免其他的异常。为什么在该方法返回nul而不是nil或者其他对象呢,因为返回NUll正好在我们的白名单里面设置了null的判断,如果返回nil的话,可能会引发其他的例如将nil插入到字典中的错误。
- 在该方法里面进行异常的上报,下次版本修复该问题。
5. 关键代码:
原理和流程我们都说了,接下来代码实现就比较简单了:
1 | // |
1 | // |
全部代码已经放到git,需要的自取: LMProtecter